/*
 * AbstractStringTemplateServlet.java
 *
 * Copyright 2010 Jack Leow
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package my.edu.clhs.stringtemplate.servlet.view;

import java.io.IOException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import my.edu.clhs.stringtemplate.renderer.*;
import my.edu.clhs.stringtemplate.servlet.ServletContextStringTemplateGroup;
import my.edu.clhs.stringtemplate.renderer.ServletStringAttributeRenderer;
import my.edu.clhs.stringtemplate.servlet.StringTemplateSupport;

import org.antlr.stringtemplate.NoIndentWriter;
import org.antlr.stringtemplate.StringTemplate;
import org.antlr.stringtemplate.StringTemplateGroup;
import org.joda.time.base.AbstractInstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract base servlet for all StringTemplate servlets.
 *
 * <p>Provides functionality common to all sub-classes, including the setting of
 * the response contentType and character encoding, and the initialization and
 * exposure of a common model based on the Servlet object model.
 *
 * <p><b><a name="init-params">Configuration <tt>init-param</tt>s</a></b>:
 * <table border="1">
 *  <tr>
 *      <th>param-name</th>
 *      <th>Description</th>
 *  </tr>
 *  <tr>
 *      <td>templateFileEncoding</td>
 *      <td>
 *          The file encoding (charset) of the source template files.
 *          If not specified StringTemplate defaults will be use.
 *      </td>
 *  </tr>
 *  <tr>
 *      <td>defaultContentType</td>
 *      <td>
 *          The default content type to set in the response header if the
 *          content type cannot be determined from the request file name.
 *          Defaults to <tt>text/html</tt>.
 *      </td>
 *  </tr>
 *  <tr>
 *      <td>outputEncoding</td>
 *      <td>
 *          The response content encoding (charset), used to encode the response
 *          content, and set in the response header.
 *          If not specified the Servlet container will use its own default.
 *      </td>
 *  </tr>
 *  <!--
 *  <tr>
 *      <td></td>
 *      <td>
 *      </td>
 *  </tr>
 * TODO Add init parameters (for AttributeRenderer customization, what else?)
 *  -->
 * </table>
 *
 * <p>The content MIME type is determined from the request URI using
 * {@link ServletContext#getMimeType(String)}.
 *
 * <p><b><a name="exposed-model">Exposed model</a></b></p> web application
 * objects are exposed to the template as attributes following JSTL EL
 * implicit object conventions where possible. Specifically:
 * <table border="1">
 *  <tr>
 *      <th>Name</th>
 *      <th>Type</th>
 *      <th>Description</th>
 *  </tr>
 *  <tr>
 *      <td><tt>pageContext.servletContext</tt></td>
 *      <td><tt>{@link javax.servlet.ServletContext}</tt></td>
 *      <td>The context for the template's servlet and any web components
 *      contained in the same application.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>pageContext.session</tt></td>
 *      <td><tt>{@link javax.servlet.http.HttpSession}</tt></td>
 *      <td>The session object for the client.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>pageContext.request</tt></td>
 *      <td><tt>{@link javax.servlet.http.HttpServletRequest}</tt></td>
 *      <td>The request triggering the invocation of the template.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>pageContext.response</tt></td>
 *      <td><tt>{@link javax.servlet.http.HttpServletResponse}</tt></td>
 *      <td>The response returned by the page.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>param</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String></tt></td>
 *      <td>Maps a request parameter name to a single value.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>paramValues</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String[]></tt></td>
 *      <td>Maps a request parameter name to an array of values.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>header</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String></tt></td>
 *      <td>Maps a request header name to a single value.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>headerValues</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String[]></tt></td>
 *      <td>Maps a request header name to an array of values.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>cookie</tt></td>
 *      <td><tt>{@link java.util.Map}<String,Cookie></tt></td>
 *      <td>Maps a cookie name to a single cookie.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>initParam</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String></tt></td>
 *      <td>Maps a context initialization parameter name to a single value.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>requestScope</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String></tt></td>
 *      <td>Maps request-scoped variable names to their values.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>sessionScope</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String></tt></td>
 *      <td>Maps session-scoped variable names to their values.</td>
 *  </tr>
 *  <tr>
 *      <td><tt>applicationScope</tt></td>
 *      <td><tt>{@link java.util.Map}<String,String></tt></td>
 *      <td>Maps application-scoped variable names to their values.</td>
 *  </tr>
 * </table>
 *
 * <p><i>Note that the <tt>pageContext</tt> and <tt>pageScope</tt> objects are
 * only applicable to JSPs. The "pageContext" is simply a Map, and is used to
 * help those who are familiar with JSTL EL conventions.</i></p>
 *
 * <p>Please refer to Appendix A.6 of the
 * <a href="http://www.jcp.org/aboutJava/communityprocess/first/jsr052">
 * JSTL 1.0 Specification</a> for details</a>.</p>
 * TODO consider not using pageContext, perhaps simply expose "request" as per facelets
 *
 * @author Jack Leow
 * @version 1.0
 * @since 1.0 (Feb 27, 2010, 9:54:47 PM)
 */
public abstract class AbstractStringTemplateServlet extends HttpServlet {
    public static final String DEFAULT_DEFAULT_CONTENT_TYPE = "text/html";
    
    private static final String TEMPLATE_FILE_ENCODING_PARAM =
        "templateFileEncoding";
    private static final String DEFAULT_OUTPUT_CONTENT_TYPE_PARAM =
        "defaultContentType";
    private static final String OUTPUT_ENCODING_PARAM =
        "outputEncoding";
    
    private static final Logger log =
        LoggerFactory.getLogger(AbstractStringTemplateServlet.class);
    private static final long serialVersionUID = 1L;
    
    protected StringTemplateGroup createTemplateLoader() {
        StringTemplateGroup templateLoader =
            new ServletContextStringTemplateGroup(null, getServletContext());
        
        if (templateFileEncoding != null) {
            templateLoader.setFileCharEncoding(templateFileEncoding);
        }
        
        return templateLoader;
    }
    
    private static final StringTemplateSupport stringTemplateSupport =
        new StringTemplateSupport();
    
    protected final StringTemplateSupport getStringTemplateSupport() {
        return stringTemplateSupport;
    }
    
    private StringTemplateGroup templateLoader;
    
    protected StringTemplateGroup getTemplateLoader() {
        if (templateLoader == null) {
            templateLoader = createTemplateLoader();
        }
        
        return templateLoader;
    }
    
    private String templateFileEncoding;
    
    protected String getTemplateFileEncoding() {
        return templateFileEncoding;
    }
    
    private String defaultContentType = DEFAULT_DEFAULT_CONTENT_TYPE;
    
    protected String getDefaultContentType() {
        return defaultContentType;
    }
    
    private String outputEncoding;
    
    protected String getOutputEncoding() {
        return outputEncoding;
    }
    
    // @Override
    public void init() throws ServletException {
        log.debug("entering init()");
        ServletConfig config = getServletConfig();
        
        templateFileEncoding =
            config.getInitParameter(TEMPLATE_FILE_ENCODING_PARAM);
        
        String defaultContentType =
            config.getInitParameter(DEFAULT_OUTPUT_CONTENT_TYPE_PARAM);
        if (defaultContentType != null) {
            this.defaultContentType = defaultContentType;
        }
        outputEncoding =
            config.getInitParameter(OUTPUT_ENCODING_PARAM);
        log.debug("exiting init()");
    }
    
    /**
     * Resolves the template name from the given request.
     * 
     * <p>This implementation resolves the template name by combining the
     * servlet path and path info (if present), and removing the file extension
     * (if present).
     *
     * <p>Subclasses can override this to customize template resolution
     * behavior.
     * 
     * @param request the request to resolve the template path from.
     * @return the template path.
     */
    protected String resolveTemplateName(HttpServletRequest request) {
        String pathInfo = request.getPathInfo();
        String requestPath = request.getServletPath() +
            (pathInfo != null ? pathInfo : "");
        int extensionIdx = requestPath.lastIndexOf('.');
        
        return extensionIdx > requestPath.lastIndexOf('/') ?
            requestPath.substring(0, extensionIdx) : requestPath;
    }
    
    /**
     * Template method that subclasses must implement to load a template for the
     * given name.
     *
     * @param templateName the name of the template to load.
     * @return the resolved template.
     */
    protected abstract StringTemplate loadTemplate(String templateName);
    
    private String determineMimeType(HttpServletRequest request) {
        log.debug("entering determineMimeType(HttpServletRequest)");
        String requestMimeType =
            getServletContext().getMimeType(request.getRequestURI());
        
        log.debug("exiting determineMimeType(HttpServletRequest)");
        return requestMimeType != null ?
            requestMimeType : getDefaultContentType();
    }
    
    /**
     * Sets the response header with the appropriate values.
     *
     * <p>This implementation simply sets the content type based on the request
     * URI's file extension.
     *
     * <p>Subclasses can override this to add additional headers.
     *
     * @param request the current request
     * @param response the current response
     */
    protected void setResponseHeaders(
            HttpServletRequest request, HttpServletResponse response) {
        log.debug("entering setResponseHeaders(" +
            "HttpServletRequest,HttpServletResponse)");
        String charEncoding = getOutputEncoding();
        
        response.setContentType(
            determineMimeType(request) +
            (charEncoding != null ? "; charset=" + charEncoding : ""));
        log.debug("exiting setResponseHeaders(" +
            "HttpServletRequest,HttpServletResponse)");
    }
    
    /**
     * Register attribute renderers for the StringTemplate.
     *
     * <p>Subclasses can override this to provide custom attribute renderer
     * registration.
     *
     * @param request the current request
     * @param response the current response
     * @param template the template to register the attribute renderers on.
     */
    protected void registerAttributeRenderers(
            HttpServletRequest request, HttpServletResponse response,
            StringTemplate template) {
        log.debug("entering registerAttributeRenderers(" +
            "HttpServletRequest,HttpServletResponse,StringTemplate)");
        // Prepare generic string renderer for all objects
        String mimeType = determineMimeType(request);
        String defaultFormatName =
            "text/html".equals(mimeType) ?
                ServletStringAttributeRenderer.HTML_ESCAPED_FORMAT :
            mimeType.substring(mimeType.indexOf('/')).indexOf("xml") > -1 ?
                ServletStringAttributeRenderer.XML_ESCAPED_FORMAT :
            null;
        ServletStringAttributeRenderer stringRenderer;
        
        if (defaultFormatName != null) {
            stringRenderer = new ServletStringAttributeRenderer(
                request, response, defaultFormatName);
        } else {
            stringRenderer =
                new ServletStringAttributeRenderer(request, response);
        }
        
        // Create the renderer mapping
        Map rendererMapping = new ClassHierarchyAwareMap();
        rendererMapping.put(Object.class, stringRenderer);
        rendererMapping.put(Number.class, new NumberAttributeRenderer());
        rendererMapping.put(Date.class, new DateAttributeRenderer());
        rendererMapping.put(Calendar.class, new CalendarAttributeRenderer());
        // If JodaTime is in the classpath
        try {
            rendererMapping.put(AbstractInstant.class,
                new JodaAbstractInstantAttributeRenderer());
        } catch (NoClassDefFoundError e) {
            // JodaTime is not in the classpath, skip
            log.debug("Joda-Time library unavailable - skipping " +
                "JodaAbstractInstantAttributeRenderer");
        }
        
        template.setAttributeRenderers(rendererMapping);
        log.debug("exiting registerAttributeRenderers(" +
            "HttpServletRequest,HttpServletResponse,StringTemplate)");
    }
    
    private static abstract class UnmodifiableEntry implements Map.Entry {
        // @Override
        public Object setValue(Object value) {
            throw new UnsupportedOperationException(
                "Values in unmodifiable entry cannot be set");
        }
    }
    
    /**
     * Exposes model objects to the StringTemplate as attributes.
     *
     * <p>This implementation exposes Servlet attributes per JSTL EL
     * conventions.
     *
     * <p>Subclasses can override this to change or add to the model.
     *
     * @param request the current request
     * @param response the current response
     * @param template the template to expose the model to
     */
    protected void exposeModel(
            final HttpServletRequest request,
            final HttpServletResponse response,
            StringTemplate template) {
        log.debug("entering exposeModel(" +
            "HttpServletRequest,HttpServletResponse,StringTemplate)");
        Map model = new HashMap();
        
        // Request, session and application scoped attributes
        Map applicationScope = new AbstractMap() {
            final ServletContext servletContext =
                request.getSession().getServletContext();
            
            // @Override
            public Object get(Object key) {
                return servletContext.getAttribute((String)key);
            }
            
            public Set entrySet() {
                Set entrySet = new HashSet();
                Enumeration names = servletContext.getAttributeNames();
                
                while (names.hasMoreElements()) {
                    final String name = (String)names.nextElement();
                    
                    entrySet.add(new UnmodifiableEntry() {
                        // @Override
                        public Object getKey() {
                            return name;
                        }
                        
                        // @Override
                        public Object getValue() {
                            return servletContext.getAttribute(name);
                        }
                    });
                }
                
                return entrySet;
            }
        };
        model.putAll(applicationScope);
        
        Map sessionScope = new AbstractMap() {
            final HttpSession session = request.getSession();
            
            // @Override
            public Object get(Object key) {
                return session.getAttribute((String)key);
            }
            
            public Set entrySet() {
                Set entrySet = new HashSet();
                Enumeration names = session.getAttributeNames();
                
                while (names.hasMoreElements()) {
                    final String name = (String)names.nextElement();
                    
                    entrySet.add(new UnmodifiableEntry() {
                        // @Override
                        public Object getKey() {
                            return name;
                        }
                        
                        // @Override
                        public Object getValue() {
                            return session.getAttribute(name);
                        }
                    });
                }
                
                return entrySet;
            }
        };
        model.putAll(sessionScope);
        
        Map requestScope = new AbstractMap() {
            // @Override
            public Object get(Object key) {
                return request.getAttribute((String)key);
            }
            
            public Set entrySet() {
                Set entrySet = new HashSet();
                Enumeration names = request.getAttributeNames();
                
                while (names.hasMoreElements()) {
                    final String name = (String)names.nextElement();
                    
                    entrySet.add(new UnmodifiableEntry() {
                        // @Override
                        public Object getKey() {
                            return name;
                        }
                        
                        // @Override
                        public Object getValue() {
                            return request.getAttribute(name);
                        }
                    });
                }
                
                return entrySet;
            }
        };
        model.putAll(requestScope);
        
        model.put("requestScope", requestScope);
        model.put("sessionScope", sessionScope);
        model.put("applicationScope", applicationScope);
        
        // Expose pseudo-pageContext
        Map pageContext = new HashMap();
        pageContext.put("servletContext", getServletContext());
        pageContext.put("session", request.getSession());
        pageContext.put("request", request);
        pageContext.put("response", response);
        model.put("pageContext", Collections.unmodifiableMap(pageContext));
        
        {
            Map param = new LinkedHashMap();
            Enumeration names = request.getParameterNames();
            
            while (names.hasMoreElements()) {
                String name = (String)names.nextElement();
                
                param.put(name, request.getParameter(name));
            }
            model.put("param", Collections.unmodifiableMap(param));
        }
        
        model.put("paramValues", request.getParameterMap());
        
        {
            Map header = new LinkedHashMap();
            Enumeration names = request.getHeaderNames();
            
            while (names.hasMoreElements()) {
                String name = (String)names.nextElement();
                
                header.put(name, request.getHeader(name));
            }
            model.put("header", Collections.unmodifiableMap(header));
        }
        
        {
            Map headerValues = new LinkedHashMap();
            Enumeration names = request.getHeaderNames();
            
            while (names.hasMoreElements()) {
                String name = (String)names.nextElement();
                
                ArrayList headersList = new ArrayList();
                Enumeration headersEnum = request.getHeaders(name);
                
                while (headersEnum.hasMoreElements()) {
                    headersList.add(headersEnum.nextElement());
                }
                
                headerValues.put(name,
                    headersList.toArray(new String[headersList.size()]));
            }
            model.put("headerValues",
                Collections.unmodifiableMap(headerValues));
        }
        
        {
            Map cookie = new LinkedHashMap();
            Cookie[] cookies = request.getCookies();
            
            if (cookies != null) {
                for (int i = 0, l = cookies.length; i < l; ++ i)  {
                    Cookie c = cookies[i];
                    
                    cookie.put(c.getName(), c);
                }
            }
            model.put("cookie", Collections.unmodifiableMap(cookie));
        }
        
        {
            Map initParam = new LinkedHashMap();
            ServletContext servletContext =
                request.getSession().getServletContext();
            Enumeration names = servletContext.getInitParameterNames();
            
            while (names.hasMoreElements()) {
                String name = (String)names.nextElement();
                
                initParam.put(name, servletContext.getInitParameter(name));
            }
            
            model.put("initParam", Collections.unmodifiableMap(initParam));
        }
        
        template.setAttributes(model);
        log.debug("exiting exposeModel(" +
            "HttpServletRequest,HttpServletResponse,StringTemplate)");
    }
    
    /**
     * Applies the template to the model and sends the results to the client.
     *
     * <p>Subclasses can override this to modify rendering behavior.
     *
     * @param response the response to send results with
     * @param template the template to apply and render
     * @throws IOException if an I/O error occurs
     */
    protected void render(
            HttpServletResponse response, StringTemplate template)
            throws IOException {
        log.debug("entering render(" +
            "HttpServletRequest,HttpServletResponse,StringTemplate)");
        template.write(new NoIndentWriter(response.getWriter()));
        log.debug("exiting render(" +
            "HttpServletRequest,HttpServletResponse,StringTemplate)");
    }
    
    /**
     * Processes the servlet request.
     *
     * @param request the request to process
     * @param response the response
     * @throws IOException if an I/O error occors
     */
    protected void processRequest(
            HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        log.debug("entering processRequest(" +
            "HttpServletRequest,HttpServletResponse)");
        StringTemplate template = loadTemplate(resolveTemplateName(request));
    
        if (template != null) {
            setResponseHeaders(request, response);
            registerAttributeRenderers(request, response, template);
            exposeModel(request, response, template);
            render(response, template);
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
        log.debug("exiting processRequest(" +
            "HttpServletRequest,HttpServletResponse)");
    }
    
    /**
     * Delegates to
     * {@link #processRequest(HttpServletRequest,HttpServletResponse)}.
     */
    // @Override
    protected final void doGet(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        log.debug("entering doGet(HttpServletRequest,HttpServletResponse)");
        processRequest(request, response);
        log.debug("exiting doGet(HttpServletRequest,HttpServletResponse)");
    }
    
    /**
     * Delegates to
     * {@link #processRequest(HttpServletRequest,HttpServletResponse)}.
     */
    // @Override
    protected final void doPost(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        log.debug("entering doPost(HttpServletRequest,HttpServletResponse)");
        processRequest(request, response);
        log.debug("exiting doPost(HttpServletRequest,HttpServletResponse)");
    }
    
    /**
     * Delegates to
     * {@link #processRequest(HttpServletRequest,HttpServletResponse)}.
     */
    // @Override
    protected final void doPut(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        log.debug("entering doPut(HttpServletRequest,HttpServletResponse)");
        processRequest(request, response);
        log.debug("exiting doPut(HttpServletRequest,HttpServletResponse)");
    }
    
    /**
     * Delegates to
     * {@link #processRequest(HttpServletRequest,HttpServletResponse)}.
     */
    // @Override
    protected final void doDelete(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        log.debug("entering doDelete(HttpServletRequest,HttpServletResponse)");
        processRequest(request, response);
        log.debug("exiting doDelete(HttpServletRequest,HttpServletResponse)");
    }
}